Express 서버의 백그라운드 작업을 명시적으로 시작하기
Express 서버의 백그라운드 작업을 명시적으로 시작하기
require() 시점의 암묵적인 실행을 없애고, 서버 부팅 과정에서 백그라운드 작업을 명시적으로 시작하는 구조를 정리한다. 단일 프로세스의 중복 실행 방지부터 다중 서버의 분산 락까지 단계적으로 살펴본다.
본문의 코드는 특정 저장소 구현을 복사하지 않고 개념을 설명하기 위해 재구성한 예시다. 이름·경로·수치는 실제 운영 정보와 무관하다.
목차
require 자체가 실행이 되는 문제
다음과 같은 모듈을 생각해 보자.
const cron = require("node-cron");
cron.schedule("0 3 * * *", refreshMetrics);
module.exports = { refreshMetrics };
이 모듈은 가져오는 순간 스케줄러를 등록한다. 문제는 “누가, 언제, 왜 이 파일을 불러왔는가”가 실행 여부를 결정한다는 점이다.
- 테스트가 함수 하나를 가져오다가 cron까지 시작한다.
- 스크립트가 공용 로직을 재사용하려다 백그라운드 작업을 중복 등록한다.
- 프로세스를 여러 개 띄우면 각 프로세스가 같은 작업을 실행한다.
- 특정 환경에서만 실행해야 하는 작업을 통제하기 어렵다.
모듈 로드와 프로그램 시작이 결합된 것이 원인이다.
등록과 실행을 분리했다
백그라운드 모듈은 스케줄러를 즉시 시작하지 않고 start()를 내보내게 만든다.
const cron = require("node-cron");
let running = false;
async function run() {
if (running) return;
running = true;
try {
await refreshMetrics();
} finally {
running = false;
}
}
function start() {
cron.schedule("0 3 * * *", run, {
timezone: "Asia/Seoul",
});
}
module.exports = { start, run };
각 작업 모듈은 자신의 실행 규칙만 알고, 전체 시작 순서는 별도의 조합 모듈이 관리한다.
function startBackgroundJobs() {
if (!backgroundJobsEnabled()) return;
require("./crawler").start();
require("./metrics").start();
require("./notifications").start();
}
이제 어떤 작업이 시작되는지 한 파일에서 볼 수 있다. 시작 순서도 의도적으로 관리할 수 있다.
HTTP 서버를 먼저 살린다
외부 검색 서버나 크롤러 초기화가 느리다고 해서 전체 HTTP 서버가 뜨지 못하는 것은 좋은 장애 격리가 아니다. 서버가 포트를 연 뒤 시작 작업을 호출하고, 부가 시스템 연결에 실패하면 fallback으로 동작하게 했다.
server.listen(port, () => {
logger.info("HTTP server started");
try {
startBackgroundJobs();
} catch (error) {
logger.error("Failed to start background jobs", { error });
}
});
물론 결제와 같은 핵심 의존성이 실패했다면 readiness check를 실패시켜야 한다. 핵심 기능과 부가 기능을 구분하는 것이 중요하다.
중복 실행 방지는 단계적으로
프로세스 하나에서는 running 플래그만으로도 앞선 작업이 종료되지 않았을 때 다음 실행을 막을 수 있다. 하지만 서버가 여러 대라면 이 플래그는 공유되지 않는다. 그 단계에서는 다음 중 하나가 필요하다.
- DB advisory lock
- 작업 레코드의 unique key
- Redis 분산 락
- 별도 worker 프로세스
처음부터 분산 락을 도입하기보다 현재 배포 구조에 맞는 최소 장치를 적용하고, 프로세스 수가 늘어나는 시점에 강화하는 편이 낫다.
메모리의 running 값은 같은 프로세스 안에서만 공유된다. PM2 cluster, Kubernetes replica처럼 프로세스가 둘 이상이면 DB나 Redis를 이용한 전역 조정 장치가 필요하다.
결론
이 리팩터링의 핵심은 cron 파일을 옮긴 것이 아니다. 시작 시점을 명시적으로 만들어 프로그램의 생명주기를 예측 가능하게 만든 것이다.
모듈을 가져오는 것은 정의를 읽는 일이어야 한다. 실제 작업을 시작하는 것은 별도의 명시적인 결정이어야 한다.
import는 정의만 읽고, 실행은 애플리케이션의 명시적인 생명주기 안에서 시작한다.
관련 노트
- express — Express의 기본 구조와 미들웨어 흐름
- 백그라운드에서의 setInterval — 브라우저 타이머가 백그라운드에서 동작하는 방식